﻿using System;
using System.Threading;
using SmartCard.Framework;

namespace SmartCard.PcscApi
{
	class PcscSmartCardReader : ISmartCardReader, IDisposable
	{
		#region events

		public event EventHandler<CardInsertedEventArgs> CardInserted = delegate { };
		public event EventHandler<CardRemovedEventArgs> CardRemoved = delegate { };
		public event EventHandler<ReaderErrorEventArgs> ReaderError = delegate { };

		#endregion

		#region fields

		/// <summary>
		/// The state of the reader during the last interval check
		/// </summary>
		private SCard_ReaderState _previousReaderState = new SCard_ReaderState();

		/// <summary>
		/// Rate that the state of the reader should be checked
		/// </summary>
		private readonly int _statusCheckRateMs;

		/// <summary>
		/// Time used to track interval check rate
		/// </summary>
		private Timer _stateCheckTimer;

		#endregion

		#region properties

		/// <summary>
		/// Get the friendly name for the reader
		/// </summary>
		public string FriendlyName
		{
			get;
			private set;
		}

		public uint InitializationContext
		{
			get;
			private set;
		}
		/// <summary>
		/// Get the reader type
		/// </summary>
		public eReaderType ReaderType
		{
			get;
			private set;
		}

		#endregion

		#region construction

		/// <summary>
		/// Create a new PC/SC Smart card reader
		/// </summary>
		/// <param name="readerName">name of the reader</param>
		/// <param name="readerType">type of reader</param>
		public PcscSmartCardReader(string readerName, eReaderType readerType)
			: this(readerName, readerType, 1000)
		{
			// get the pcsc context
			InitializationContext = EstablishContext();
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="readerName">name of the reader</param>
		/// <param name="readerType">type of reader</param>
		/// <param name="statusCheckRateMs">interval rate that card insertions/removals should be checked at</param>
		public PcscSmartCardReader(string readerName, eReaderType readerTime, int statusCheckRateMs)
		{
			if (string.IsNullOrWhiteSpace(readerName))
			{
				throw new ArgumentException("reader name can not be null or empty", "readerName");
			}

			if (statusCheckRateMs <= 0)
			{
				throw new ArgumentOutOfRangeException("value must be greater than zero", "statusCheckRateMs");
			}

			FriendlyName = readerName;
			ReaderType = ReaderType;
			_statusCheckRateMs = statusCheckRateMs;

			// create the timer that will used for notifications
			_stateCheckTimer = new Timer(UpdateReaderStatus, null, _statusCheckRateMs, Timeout.Infinite);
		}

		#endregion

		#region public methods

		public ICard GetInsertedCard()
		{
			ICard insertedCard = null;

			var readerState = GetReaderState();
			var state = ConvertPcscStateToCardReaderState(readerState.m_dwEventState);

			if (state == eSmartCardReaderState.CardPresent)
			{
				byte[] atr = new byte[readerState.m_cbAtr];
				Array.Copy(readerState.m_rgbAtr, atr, atr.Length);
				insertedCard = new PcscSmartCard(this, atr);
			}

			return insertedCard;
		}

		public bool ContainsCard()
		{
			var readerState = GetReaderState();
			var state = ConvertPcscStateToCardReaderState(readerState.m_dwEventState);

			return (state == eSmartCardReaderState.CardPresent);
		}

		#endregion

		#region privates and protected

		/// <summary>
		/// Returns the readerstate structure for all of the reader names that are passed in.
		/// </summary>
		/// <param name="context"></param>
		/// <param name="readerNames"></param>
		/// <returns></returns>
		private SCard_ReaderState GetReaderState()
		{
			// init the reader state strucutre
			var readerState = new SCard_ReaderState[]
				{ new SCard_ReaderState()
					{
						m_szReader = FriendlyName,
						m_pvUserData = IntPtr.Zero,
						m_dwCurrentState = 0
					}
				};

			int retVal = PcscInvokes.SCardGetStatusChange(InitializationContext, 0, readerState, (uint)readerState.Length);

			// check for error
			if ((ePcscErrors)retVal != ePcscErrors.Success)
			{ throw new PcscException("Error code returned from SCardGetStatusChange", (ePcscErrors)retVal); }

			return readerState[0];
		}

		private void UpdateReaderStatus(object state)
		{
			try
			{
				var previousState = _previousReaderState;
				_previousReaderState = GetReaderState();

				// if the previous state was null, then we will never send a notification.  this is
				// some init setup and we don't want to bombard the API user with startup messages to ignore
				// the startup user should explicitly ask for the state
				if (!string.IsNullOrWhiteSpace(previousState.m_szReader))
				{
					// for now we will only pay attention to the state of the reader 
					var newState = ConvertPcscStateToCardReaderState(_previousReaderState.m_dwEventState);
					var prevState = ConvertPcscStateToCardReaderState(previousState.m_dwEventState);

					// decide if we need to send a notifiation
					if (newState != prevState)
					{
						if (newState == eSmartCardReaderState.NoCardPresent)
						{
							SendCardRemovedEvent(new CardRemovedEventArgs());
						}
						else if (newState == eSmartCardReaderState.CardPresent)
						{
							byte[] atr = new byte[previousState.m_cbAtr];

							if (previousState.m_rgbAtr != null)
							{ Array.Copy(previousState.m_rgbAtr, atr, atr.Length); }

							SendCardInsertedEvent(new CardInsertedEventArgs(
								new PcscSmartCard(this, atr)));
						}
					}
				}
			}
			finally
			{
				// trigger the timer to run again
				_stateCheckTimer.Change(_statusCheckRateMs, Timeout.Infinite);
			}
		}

		private static eSmartCardReaderState ConvertPcscStateToCardReaderState(uint eventState)
		{
			var retVal = eSmartCardReaderState.NoCardPresent;

			if ((eventState & 4) != 0)
			{ retVal = eSmartCardReaderState.Unknown; }
			else if ((eventState & 16) != 0)
			{ retVal = eSmartCardReaderState.NoCardPresent; }
			else if ((eventState & 32) != 0)
			{ retVal = eSmartCardReaderState.CardPresent; }

			return retVal;
		}

		protected virtual void SendCardInsertedEvent(CardInsertedEventArgs eventArgs)
		{
			CardInserted(this, eventArgs);
		}

		protected virtual void SendCardRemovedEvent(CardRemovedEventArgs eventArgs)
		{
			CardRemoved(this, eventArgs);
		}

		private UInt32 EstablishContext()
		{
			UInt32 context;
			var ret = PcscInvokes.SCardEstablishContext(
				(uint)ePcscScope.User, IntPtr.Zero, IntPtr.Zero, out context);

			if ((ePcscErrors)ret != ePcscErrors.Success)
			{ throw new PcscException("Unable to establish PC/SC context", (ePcscErrors)ret); }

			return context;
		}

		private void ReleaseContextSafe()
		{
			var initializationContext = InitializationContext;
			InitializationContext = Int32.MaxValue;
			var ret = PcscInvokes.SCardReleaseContext(initializationContext);
			// don't throw safe operation
		}

		#endregion

		#region disposable pattern

		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		protected virtual void Dispose(bool disposing)
		{
			if (disposing)
			{
				if (_stateCheckTimer != null)
				{
					_stateCheckTimer.Change(Timeout.Infinite, Timeout.Infinite);
					_stateCheckTimer.Dispose();
					_stateCheckTimer = null;
				}
			}

			ReleaseContextSafe();
		}

		#endregion
	}
}
